//----------------------------------------------------------------------------//
//                                                                            //
// ozz-animation is hosted at http://github.com/guillaumeblanc/ozz-animation  //
// and distributed under the MIT License (MIT).                               //
//                                                                            //
// Copyright (c) Guillaume Blanc                                              //
//                                                                            //
// Permission is hereby granted, free of charge, to any person obtaining a    //
// copy of this software and associated documentation files (the "Software"), //
// to deal in the Software without restriction, including without limitation  //
// the rights to use, copy, modify, merge, publish, distribute, sublicense,   //
// and/or sell copies of the Software, and to permit persons to whom the      //
// Software is furnished to do so, subject to the following conditions:       //
//                                                                            //
// The above copyright notice and this permission notice shall be included in //
// all copies or substantial portions of the Software.                        //
//                                                                            //
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR //
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,   //
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL    //
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER //
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING    //
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER        //
// DEALINGS IN THE SOFTWARE.                                                  //
//                                                                            //
//----------------------------------------------------------------------------//

#include "SkrAnim/ozz/track_triggering_job.h"

#include <algorithm>
#include <cassert>

#include "SkrAnim/ozz/track.h"

namespace ozz
{
namespace animation
{

bool TrackTriggeringJob::Validate() const
{
    bool valid = true;
    valid &= track != nullptr;
    valid &= iterator != nullptr;
    return valid;
}

bool TrackTriggeringJob::Run() const
{
    if (!Validate())
    {
        return false;
    }

    // Triggering can only happen in a valid range of ratio.
    if (from == to)
    {
        *iterator = end();
        return true;
    }

    *iterator = Iterator(this);

    return true;
}

namespace
{
inline bool DetectEdge(ptrdiff_t _i0, ptrdiff_t _i1, bool _forward, const TrackTriggeringJob& _job, TrackTriggeringJob::Edge* _edge)
{
    const span<const float>& values = _job.track->values();

    const float vk0 = values[_i0];
    const float vk1 = values[_i1];

    bool detected = false;
    if (vk0 <= _job.threshold && vk1 > _job.threshold)
    {
        // Rising edge
        _edge->rising = _forward;
        detected      = true;
    }
    else if (vk0 > _job.threshold && vk1 <= _job.threshold)
    {
        // Falling edge
        _edge->rising = !_forward;
        detected      = true;
    }

    if (detected)
    {
        const span<const float>&   ratios = _job.track->ratios();
        const span<const uint8_t>& steps  = _job.track->steps();

        const bool step = (steps[_i0 / 8] & (1 << (_i0 & 7))) != 0;
        if (step)
        {
            _edge->ratio = ratios[_i1];
        }
        else
        {
            assert(vk0 != vk1); // Won't divide by 0

            if (_i1 == 0)
            {
                _edge->ratio = 0.f;
            }
            else
            {
                // Finds where the curve crosses threshold value.
                // This is the lerp equation, where we know the result and look for
                // alpha, aka un-lerp.
                const float alpha = (_job.threshold - vk0) / (vk1 - vk0);

                // Remaps to keyframes actual times.
                const float tk0 = ratios[_i0];
                const float tk1 = ratios[_i1];
                _edge->ratio    = math::Lerp(tk0, tk1, alpha);
            }
        }
    }
    return detected;
}
} // namespace

TrackTriggeringJob::Iterator::Iterator(const TrackTriggeringJob* _job)
    : job_(_job)
{
    // Outer loop initialization.
    outer_ = floorf(job_->from);

    // Search could start more closely to the "from" ratio, but it's not possible
    // to ensure that floating point precision will not lead to missing a key
    // (when from/to range is far from 0). This is less good in algorithmic
    // complexity, but for consistency of forward and backward triggering, it's
    // better to let iterator ++ implementation filter included and excluded
    // edges.
    inner_ = job_->from < job_->to ? 0 : _job->track->ratios().size() - 1;

    // Evaluates first edge.
    ++*this;
}

const TrackTriggeringJob::Iterator& TrackTriggeringJob::Iterator::operator++()
{
    assert(*this != job_->end() && "Can't increment end iterator.");

    const span<const float>& ratios   = job_->track->ratios();
    const ptrdiff_t          num_keys = ratios.size();

    if (job_->to > job_->from)
    {
        for (; outer_ < job_->to; outer_ += 1.f)
        {
            for (; inner_ < num_keys; ++inner_)
            {
                const ptrdiff_t i0 = inner_ == 0 ? num_keys - 1 : inner_ - 1;
                if (DetectEdge(i0, inner_, true, *job_, &edge_))
                {
                    edge_.ratio += outer_; // Convert to global ratio space.
                    if (edge_.ratio >= job_->from &&
                        (edge_.ratio < job_->to || job_->to >= 1.f + outer_))
                    {
                        ++inner_;
                        return *this; // Yield found edge.
                    }
                    // Won't find any further edge.
                    if (ratios[inner_] + outer_ >= job_->to)
                    {
                        break;
                    }
                }
            }
            inner_ = 0; // Ready for next loop.
        }
    }
    else
    {
        for (; outer_ + 1.f > job_->to; outer_ -= 1.f)
        {
            for (; inner_ >= 0; --inner_)
            {
                const ptrdiff_t i0 = inner_ == 0 ? num_keys - 1 : inner_ - 1;
                if (DetectEdge(i0, inner_, false, *job_, &edge_))
                {
                    edge_.ratio += outer_; // Convert to global ratio space.
                    if (edge_.ratio >= job_->to &&
                        (edge_.ratio < job_->from || job_->from >= 1.f + outer_))
                    {
                        --inner_;
                        return *this; // Yield found edge.
                    }
                }
                // Won't find any further edge.
                if (ratios[inner_] + outer_ <= job_->to)
                {
                    break;
                }
            }
            inner_ = ratios.size() - 1; // Ready for next loop.
        }
    }

    // Set iterator to end position.
    *this = job_->end();

    return *this;
}
} // namespace animation
} // namespace ozz
